package com.fary.aop.aspectj.annotation;

import com.fary.aop.Advisor;
import com.fary.beans.factory.BeanFactoryUtils;
import com.fary.beans.factory.ListableBeanFactory;
import com.fary.util.Assert;
import com.sun.istack.internal.Nullable;
import org.aspectj.lang.reflect.PerClauseKind;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class BeanFactoryAspectJAdvisorsBuilder {

    private final ListableBeanFactory beanFactory;

    private final AspectJAdvisorFactory advisorFactory;

    private volatile List<String> aspectBeanNames;

    private final Map<String, List<Advisor>> advisorsCache = new ConcurrentHashMap<>();

    private final Map<String, MetadataAwareAspectInstanceFactory> aspectFactoryCache = new ConcurrentHashMap<>();


    /**
     * Create a new BeanFactoryAspectJAdvisorsBuilder for the given BeanFactory.
     *
     * @param beanFactory the ListableBeanFactory to scan
     */
    public BeanFactoryAspectJAdvisorsBuilder(ListableBeanFactory beanFactory) {
        this(beanFactory, new ReflectiveAspectJAdvisorFactory(beanFactory));
    }

    /**
     * Create a new BeanFactoryAspectJAdvisorsBuilder for the given BeanFactory.
     *
     * @param beanFactory    the ListableBeanFactory to scan
     * @param advisorFactory the AspectJAdvisorFactory to build each Advisor with
     */
    public BeanFactoryAspectJAdvisorsBuilder(ListableBeanFactory beanFactory, AspectJAdvisorFactory advisorFactory) {
        Assert.notNull(beanFactory, "ListableBeanFactory must not be null");
        Assert.notNull(advisorFactory, "AspectJAdvisorFactory must not be null");
        this.beanFactory = beanFactory;
        this.advisorFactory = advisorFactory;
    }


    /**
     * Look for AspectJ-annotated aspect beans in the current bean factory,
     * and return to a list of Spring AOP Advisors representing them.
     * <p>Creates a Spring Advisor for each AspectJ advice method.
     *
     * @see #isEligibleBean
     */
    public List<Advisor> buildAspectJAdvisors() {
        // aspectBeanNames 中缓存了被 @Aspect 修饰的 bean 的name
        List<String> aspectNames = this.aspectBeanNames;
        // 如果为空表示尚未缓存，进行缓存解析。这里用了DLC 方式来进行判断
        if (aspectNames == null) {
            synchronized (this) {
                aspectNames = this.aspectBeanNames;
                if (aspectNames == null) {
                    List<Advisor> advisors = new ArrayList<>();
                    aspectNames = new ArrayList<>();
                    // 1. 获取所有的beanName。从容器中获取所有的BeanName
                    String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Object.class, true, false);
                    // 遍历beanName, 找出对应的增强方法
                    for (String beanName : beanNames) {
                        // 不合法的bean略过，由子类定义规则，默认true
                        if (!isEligibleBean(beanName)) {
                            continue;
                        }
                        // 注释 ：我们必须小心，不要急于实例化bean，因为在这种情况下，它们将由Spring容器缓存，但不会被编织
                        // 获取对应 bean 的类型
                        Class<?> beanType = this.beanFactory.getType(beanName);
                        if (beanType == null) {
                            continue;
                        }
                        // 2. 如果bean 被 @AspectJ 注解修饰 且不是Ajc 编译, 则进一步处理
                        if (this.advisorFactory.isAspect(beanType)) {
                            // 添加到缓存中
                            aspectNames.add(beanName);
                            // 封装成AspectMetadata
                            AspectMetadata amd = new AspectMetadata(beanType, beanName);
                            // aspect 存在 SINGLETON、PERTHIS、PERTARGET、PERCFLOW、PERCFLOWBELOW、PERTYPEWITHIN模式。默认为SINGLETON 。暂不明白意义
                            if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
                                MetadataAwareAspectInstanceFactory factory = new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
                                // 3. 解析标记AspectJ注解中的增强方法，也就是被 @Before、@Around 等注解修饰的方法，并将其封装成 Advisor
                                List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
                                if (this.beanFactory.isSingleton(beanName)) {
                                    this.advisorsCache.put(beanName, classAdvisors);
                                } else {
                                    this.aspectFactoryCache.put(beanName, factory);
                                }
                                // 保存 Advisor
                                advisors.addAll(classAdvisors);
                            } else {
                                // 如果当前Bean是单例，但是 Aspect 不是单例则抛出异常
                                if (this.beanFactory.isSingleton(beanName)) {
                                    throw new IllegalArgumentException("Bean with name '" + beanName + "' is a singleton, but aspect instantiation model is not singleton");
                                }
                                MetadataAwareAspectInstanceFactory factory = new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
                                this.aspectFactoryCache.put(beanName, factory);
                                advisors.addAll(this.advisorFactory.getAdvisors(factory));
                            }
                        }
                    }
                    this.aspectBeanNames = aspectNames;
                    return advisors;
                }
            }
        }

        if (aspectNames.isEmpty()) {
            return Collections.emptyList();
        }
        // 4. 将所有的增强方法从缓存中取出。
        List<Advisor> advisors = new ArrayList<>();
        for (String aspectName : aspectNames) {
            List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
            if (cachedAdvisors != null) {
                advisors.addAll(cachedAdvisors);
            } else {
                MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
                advisors.addAll(this.advisorFactory.getAdvisors(factory));
            }
        }
        return advisors;
    }

    /**
     * Return whether the aspect bean with the given name is eligible.
     *
     * @param beanName the name of the aspect bean
     * @return whether the bean is eligible
     */
    protected boolean isEligibleBean(String beanName) {
        return true;
    }

}